/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.uberfire.client.workbench;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.Window.ClosingHandler;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import org.jboss.errai.bus.client.api.ClientMessageBus;
import org.jboss.errai.bus.client.framework.ClientMessageBusImpl;
import org.jboss.errai.ioc.client.api.AfterInitialization;
import org.jboss.errai.ioc.client.api.EnabledByProperty;
import org.jboss.errai.ioc.client.api.EntryPoint;
import org.jboss.errai.ioc.client.container.SyncBeanDef;
import org.jboss.errai.ioc.client.container.SyncBeanManager;
import org.jboss.errai.security.shared.api.identity.User;
import org.slf4j.Logger;
import org.uberfire.backend.vfs.Path;
import org.uberfire.client.mvp.PerspectiveActivity;
import org.uberfire.client.mvp.PlaceManager;
import org.uberfire.client.resources.WorkbenchResources;
import org.uberfire.client.workbench.events.ApplicationReadyEvent;
import org.uberfire.mvp.ParameterizedCommand;
import org.uberfire.mvp.impl.DefaultPlaceRequest;
import org.uberfire.mvp.impl.PathPlaceRequest;
import org.uberfire.rpc.SessionInfo;
import org.uberfire.rpc.impl.SessionInfoImpl;
import org.uberfire.security.authz.AuthorizationManager;
import org.uberfire.security.authz.AuthorizationPolicy;
import org.uberfire.security.authz.PermissionManager;
/**
* Responsible for bootstrapping the client-side Workbench user interface by coordinating calls to the PanelManager and
* PlaceManager. Normally this happens automatically with no need for assistance or interference from the application.
* Thus, applications don't usually need to do anything with the Workbench class directly.
* <p>
* <h2>Delaying Workbench Startup</h2>
* <p>
* In special cases, applications may wish to delay the startup of the workbench. For example, an application that
* relies on global variables (also known as singletons or Application Scoped beans) that are initialized based on
* response data from the server doesn't want UberFire to start initializing its widgets until that server response has
* come in.
* <p>
* To delay startup, add a <i>Startup Blocker</i> before Errai starts calling {@link AfterInitialization} methods. The
* best place to do this is in the {@link PostConstruct} method of an {@link EntryPoint} bean. You would then remove the
* startup blocker from within the callback from the server:
* <p>
* <pre>
* {@code @EntryPoint}
* public class MyMutableGlobal() {
* {@code @Inject private Workbench workbench;}
* {@code @Inject private Caller<MyRemoteService> remoteService;}
*
* // set up by a server call. don't start the app until it's populated!
* {@code private MyParams params;}
*
* {@code @PostConstruct}
* private void earlyInit() {
* workbench.addStartupBlocker(MyMutableGlobal.class);
* }
*
* {@code @AfterInitialization}
* private void lateInit() {
* remoteService.call(new {@code RemoteCallback<MyParams>}{
* public void callback(MyParams params) {
* MyMutableGlobal.this.params = params;
* workbench.removeStartupBlocker(MyMutableGlobal.class);
* }
* }).fetchParameters();
* }
* }
* </pre>
*/
@EntryPoint
@EnabledByProperty(value = "uberfire.plugin.mode.active", negated = true)
public class Workbench {
/**
* List of classes who want to do stuff (often server communication) before the workbench shows up.
*/
private final Set<Class<?>> startupBlockers = new HashSet<Class<?>>();
private final Set<String> headersToKeep = new HashSet<String>();
/**
* This indirection exists so we can ignore spurious WindowCloseEvents in IE10.
* In all other cases, the {@link WorkbenchCloseHandler} simply executes whatever command we pass it.
*/
private final WorkbenchCloseHandler workbenchCloseHandler = GWT.create(WorkbenchCloseHandler.class);
@Inject
LayoutSelection layoutSelection;
/**
* Fired when all startup blockers have cleared and just before the workbench starts to build its components.
*/
@Inject
private Event<ApplicationReadyEvent> appReady;
private boolean isStandaloneMode = false;
@Inject
private SyncBeanManager iocManager;
@Inject
private PlaceManager placeManager;
private final Command workbenchCloseCommand = new Command() {
@Override
public void execute() {
placeManager.closeAllPlaces(); // would be preferable to close current perspective, which should be recursive
}
};
@Inject
private PermissionManager permissionManager;
@Inject
private AuthorizationManager authorizationManager;
@Inject
private VFSServiceProxy vfsService;
private WorkbenchLayout layout;
@Inject
private User identity;
@Inject
private ClientMessageBus bus;
@Inject
private Logger logger;
private SessionInfo sessionInfo = null;
/**
* Requests that the workbench does not attempt to create any UI parts until the given responsible party has
* been removed as a startup blocker. Blockers are tracked as a set, so adding the same class more than once has no
* effect.
* @param responsibleParty any Class object; typically it will be the class making the call to this method.
* Must not be null.
*/
public void addStartupBlocker(Class<?> responsibleParty) {
startupBlockers.add(responsibleParty);
System.out.println(responsibleParty.getName() + " is blocking workbench startup.");
}
/**
* Causes the given responsible party to no longer block workbench initialization.
* If the given responsible party was not already in the blocking set (either because
* it was never added, or it has already been removed) then the method call has no effect.
* <p>
* After removing the blocker, if there are no more blockers left in the blocking set, the workbench UI is
* bootstrapped immediately. If there are still one or more blockers left in the blocking set, the workbench UI
* remains uninitialized.
* @param responsibleParty any Class object that was previously passed to {@link #addStartupBlocker(Class)}.
* Must not be null.
*/
public void removeStartupBlocker(Class<?> responsibleParty) {
if (startupBlockers.remove(responsibleParty)) {
System.out.println(responsibleParty.getName() + " is no longer blocking startup.");
} else {
System.out.println(responsibleParty.getName() + " tried to unblock startup, but it wasn't blocking to begin with!");
}
startIfNotBlocked();
}
// package-private so tests can call in
@AfterInitialization
void startIfNotBlocked() {
System.out.println(startupBlockers.size() + " workbench startup blockers remain.");
if (startupBlockers.isEmpty()) {
bootstrap();
}
}
@PostConstruct
private void earlyInit() {
layout = layoutSelection.get();
WorkbenchResources.INSTANCE.CSS().ensureInjected();
isStandaloneMode = Window.Location.getParameterMap().containsKey("standalone");
for (final Map.Entry<String, List<String>> parameter : Window.Location.getParameterMap().entrySet()) {
if (parameter.getKey().equals("header")) {
headersToKeep.addAll(parameter.getValue());
}
}
}
private void bootstrap() {
logger.info("Starting workbench...");
((SessionInfoImpl) currentSession()).setId(((ClientMessageBusImpl) bus).getSessionId());
layout.setMarginWidgets(isStandaloneMode,
headersToKeep);
layout.onBootstrap();
addLayoutToRootPanel(layout);
//Lookup PerspectiveProviders and if present launch it to set-up the Workbench
if (!isStandaloneMode) {
final PerspectiveActivity homePerspective = getHomePerspectiveActivity();
if (homePerspective != null) {
appReady.fire(new ApplicationReadyEvent());
placeManager.goTo(new DefaultPlaceRequest(homePerspective.getIdentifier()));
} else {
logger.error("No home perspective available!");
}
} else {
handleStandaloneMode(Window.Location.getParameterMap());
}
// Ensure orderly shutdown when Window is closed (eg. saves workbench state)
Window.addWindowClosingHandler(new ClosingHandler() {
@Override
public void onWindowClosing(ClosingEvent event) {
workbenchCloseHandler.onWindowClose(workbenchCloseCommand);
}
});
// Resizing the Window should resize everything
Window.addResizeHandler(new ResizeHandler() {
@Override
public void onResize(ResizeEvent event) {
layout.resizeTo(event.getWidth(),
event.getHeight());
}
});
// Defer the initial resize call until widgets are rendered and sizes are available
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
layout.onResize();
}
});
}
// TODO add tests for standalone startup vs. full startup
private void handleStandaloneMode(final Map<String, List<String>> parameters) {
if (parameters.containsKey("perspective") && !parameters.get("perspective").isEmpty()) {
placeManager.goTo(new DefaultPlaceRequest(parameters.get("perspective").get(0)));
} else if (parameters.containsKey("path") && !parameters.get("path").isEmpty()) {
placeManager.goTo(new DefaultPlaceRequest("StandaloneEditorPerspective"));
vfsService.get(parameters.get("path").get(0),
new ParameterizedCommand<Path>() {
@Override
public void execute(final Path response) {
if (parameters.containsKey("editor") && !parameters.get("editor").isEmpty()) {
placeManager.goTo(new PathPlaceRequest(response,
parameters.get("editor").get(0)));
} else {
placeManager.goTo(new PathPlaceRequest(response));
}
}
});
}
}
/**
* Get the home perspective defined at the workbench authorization policy.
* <p>
* <p>If no home is defined then the perspective marked as "{@code isDefault=true}" is taken.</p>
* <p>
* <p>Notice that access permission over the selected perspective is always required.</p>
* @return A perspective instance or null if no perspective is found or access to it has been denied.
*/
public PerspectiveActivity getHomePerspectiveActivity() {
// Get the user's home perspective
PerspectiveActivity homePerspective = null;
AuthorizationPolicy authPolicy = permissionManager.getAuthorizationPolicy();
String homePerspectiveId = authPolicy.getHomePerspective(identity);
// Get the workbench's default perspective
PerspectiveActivity defaultPerspective = null;
final Collection<SyncBeanDef<PerspectiveActivity>> perspectives = iocManager.lookupBeans(PerspectiveActivity.class);
for (final SyncBeanDef<PerspectiveActivity> perspective : perspectives) {
final PerspectiveActivity instance = perspective.getInstance();
if (homePerspectiveId != null && homePerspectiveId.equals(instance.getIdentifier())) {
homePerspective = instance;
if (defaultPerspective != null) {
iocManager.destroyBean(defaultPerspective);
}
} else if (instance.isDefault()) {
defaultPerspective = instance;
} else {
iocManager.destroyBean(instance);
}
}
// The home perspective has always priority over the default
PerspectiveActivity targetPerspective = homePerspective != null ? homePerspective : defaultPerspective;
// Check access rights
if (targetPerspective != null && authorizationManager.authorize(targetPerspective,
identity)) {
return targetPerspective;
}
return null;
}
@Produces
@ApplicationScoped
private SessionInfo currentSession() {
if (sessionInfo == null) {
sessionInfo = new SessionInfoImpl(identity);
}
return sessionInfo;
}
void addLayoutToRootPanel(final WorkbenchLayout layout) {
RootLayoutPanel.get().add(layout.getRoot());
}
}